From 72e52a7d038d7f0abf31fcf6233bf5ec214c1003 Mon Sep 17 00:00:00 2001 From: Donovan Date: Mon, 15 Dec 2025 16:31:53 -0600 Subject: [PATCH] partial implement client app --- Cargo.lock | 69 +++++++ Cargo.toml | 5 + nkode-rs | 2 +- src/app/client.rs | 189 ++++++++++++------ src/models/app.rs | 55 +++++ src/models/email.rs | 110 ++++++++++ src/models/mod.rs | 4 +- src/models/opaque.rs | 32 ++- src/opaque/client.rs | 4 +- src/opaque/server.rs | 7 +- .../opaque/in_memory/in_memory_transport.rs | 14 +- 11 files changed, 416 insertions(+), 75 deletions(-) create mode 100644 src/models/app.rs create mode 100644 src/models/email.rs diff --git a/Cargo.lock b/Cargo.lock index abc2ba3..8a8ae36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,18 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.89" @@ -71,6 +83,19 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -102,6 +127,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -124,6 +159,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -282,6 +323,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "ff" version = "0.13.1" @@ -298,6 +348,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "generic-array" version = "0.14.7" @@ -405,10 +461,15 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" name = "nkode-protocol" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", + "blake3", + "email_address", + "getset", "nkode-rs", "opaque-ke", "rand 0.8.5", + "serde", "sha2", "tokio", "uuid", @@ -424,7 +485,9 @@ dependencies = [ "getset", "rand 0.9.2", "rand_chacha 0.9.0", + "serde", "sha2", + "zeroize", ] [[package]] @@ -686,6 +749,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index cb8c222..cdc726e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,5 +12,10 @@ uuid = { version = "1.19.0", features = ["v4"] } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync"] } nkode-rs = { path = "nkode-rs" } zeroize = "1.8.2" +blake3 = "1.8.2" +email_address = "0.2.9" +anyhow = "1.0.100" +serde = { version = "1.0.228", features = ["derive"] } +getset = "0.1.6" diff --git a/nkode-rs b/nkode-rs index 13f007a..e5712b4 160000 --- a/nkode-rs +++ b/nkode-rs @@ -1 +1 @@ -Subproject commit 13f007abf7a84c4bf75397b253f6b29aaff30dcf +Subproject commit e5712b4a92f1363a1cb7692e15cb74f6dfbef0eb diff --git a/src/app/client.rs b/src/app/client.rs index b53dd6f..ef58a40 100644 --- a/src/app/client.rs +++ b/src/app/client.rs @@ -1,53 +1,36 @@ -use uuid::Uuid; -use zeroize::Zeroizing; +use nkode_rs::nkode_core::keypad::Keypad; +use crate::models::app::{CodeLoginData, CodeLoginSession, Icon, IconID, KeyLoginSession, ICON_ID_SIZE}; +use crate::models::email::Email; +use crate::models::opaque::UserSecretKey; +use anyhow::Result; +use nkode_rs::nkode_core::nkode_cipher::NKodeCipher; +use nkode_rs::nkode_core::policy::NKodePolicy; +use nkode_rs::from_bytes::FromBytes; +use nkode_rs::nkode_core::chacha20prng::Nonce; - -struct LoginSession { - identity: Vec, - session_id: Uuid, - session_key: Zeroizing>, -} - -struct KeyLoginSession(LoginSession); -struct CodeLoginSession(LoginSession); - -struct IconID(u128); - -struct Icon { - id: IconID, - data: Vec, -} - -struct CodeLoginData { - mask: Vec, - icons: Vec, - nonce: Vec, - keypad_indices: Vec -} - -trait ServerAPI { - async fn register_key(&self, identity: &[u8], secret_key: &[u8]) -> Result<(), String>; - async fn register_code(&self, identity: &[u8], passcode: &[u64]) -> Result<(), String>; - async fn login_key(&self, identity: &[u8], secret_key: &[u8]) -> Result; - async fn login_code(&self, identity: &[u8], passcode: &[u64]) -> Result; +pub trait ServerAPI { + async fn register_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result<(), String>; + async fn register_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoginSession, data: &CodeLoginData) -> Result<(), String>; + async fn login_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result; + async fn login_code(&self, email: &Email, passcode: &[u64]) -> Result; async fn get_new_icons(&self, key_login_session: &KeyLoginSession) -> Result, String>; - async fn set_login_data(&self, key_login_session: &KeyLoginSession, data: &CodeLoginData) -> Result<(), String>; async fn get_login_data(&self, key_login_session: &KeyLoginSession) -> Result; async fn is_code_registered(&self, key_login_session: &KeyLoginSession) -> Result; + async fn get_policy(&self) -> Result; } pub struct RegisterKey; impl RegisterKey { - pub async fn register(api: S,identity: &[u8], secret_key: &[u8]) -> Result, String> { + pub async fn register(api: S, email: Email, user_secret_key: &UserSecretKey) -> Result, String> { todo!() } } pub struct KeyLogin { api: S, - identity: Vec, - secret_key: Zeroizing> + email: Email, + user_secret_key: UserSecretKey } impl KeyLogin { @@ -66,7 +49,7 @@ impl KeyLogin { pub struct RegisterCode { api: S, - secret_key: Vec, + user_secret_key: UserSecretKey, key_login_session: KeyLoginSession, } @@ -75,7 +58,7 @@ impl RegisterCode { todo!() } - pub async fn register(self, identity: &[u8], passcode: &[u64]) -> Result { + pub async fn register(self, email: Email, passcode: &[u64]) -> Result { todo!() } } @@ -85,43 +68,133 @@ pub struct CodeLogin { } impl CodeLogin { - pub async fn login(self, identity: &[u8], passcode: &[u64]) -> Result { + pub async fn login(self, email: Email, passcode: &[u64]) -> Result { todo!() } } -trait ClientRepo { - async fn get_secret_key(&self) -> Result, String>; - async fn set_secret_key(&mut self, secret_key: &[u8]) -> Result<(),String>; +pub trait ClientRepo { + async fn get_secret_key(&self) -> Result; + async fn set_secret_key(&self, email: &Email,user_secret_key: &UserSecretKey) -> Result<(),String>; async fn get_login_data(&self) -> Result; - async fn set_login_data(&mut self, data: CodeLoginData) -> Result<(), String>; - async fn set_identity(&mut self, identity: &[u8]) -> Result<(), String>; - async fn get_identity(&self) -> Result<&[u8], String>; + async fn set_login_data(&self, data: CodeLoginData) -> Result<(), String>; + async fn set_email(&self, email: &Email) -> Result<(), String>; + async fn get_email(&self) -> Result; } -pub struct ClientApp { +pub struct ClientAppKey { repo: R, - api: S + api: S, + email: Email, + user_secret_key: UserSecretKey, + key_login: KeyLoginSession, } -impl ClientApp { - pub fn new_secret() -> Vec { - todo!() +impl ClientAppKey { + pub async fn new_register(email: Email, user_secret_key: UserSecretKey, repo: R, api: S) -> Result { + repo.set_secret_key(&email, &user_secret_key).await?; + api.register_key(&email, &user_secret_key).await?; + let key_login = api.login_key(&email, &user_secret_key).await?; + Ok(Self { + repo, + api, + email, + user_secret_key, + key_login, + }) } - pub async fn register_secret(&mut self, identity: &[u8], secret_key: &[u8]) -> Result<(), String> { - todo!() + pub async fn register_code(self) -> Result, String> { + let icon_nonce = Nonce::new(); + let icons = self.get_icons(&icon_nonce).await?; + let policy = self.api.get_policy().await?; + Ok(ClientAppCodeRegister { + api: self.api, + email: self.email, + user_secret_key: self.user_secret_key, + key_login: self.key_login, + icons, + icon_nonce, + keypad: Keypad::new(policy), + }) } - pub async fn get_new_icons(&self) -> Result, String> { - todo!() + async fn get_icons(&self, icon_nonce: &Nonce) -> Result, String> { + let chacha20_key = self.user_secret_key.chacha20_secret_key(); + let mut chacha_cipher = nkode_rs::nkode_core::chacha20prng::ChaCha20PRNG::new(&chacha20_key, icon_nonce); + let mut icons = self.api.get_new_icons(&self.key_login).await?; + for icon in &mut icons { + let bytes = chacha_cipher.read_u8(ICON_ID_SIZE)?; + let new_id = IconID::from_bytes(bytes.as_slice()).unwrap(); + icon.update_id(new_id); + } + Ok(icons) } - pub async fn select_icons(&mut self, selected_icons: Vec) -> Result<(), String> { - todo!() - } - - pub async fn set_secret(&mut self, secret_key: &[u8]) -> Result<(), String> { + pub async fn login_code(self) -> Result, String> { todo!() } } + +pub struct ClientAppCodeRegister { + api: S, + email: Email, + user_secret_key: UserSecretKey, + key_login: KeyLoginSession, + icon_nonce: Nonce, + icons: Vec, + keypad: Keypad, +} + +impl ClientAppCodeRegister { + pub async fn register(self, selected_icons: Vec) -> Result, String> { + let policy = self.api.get_policy().await?; + let keypad = Keypad::new(policy.clone()); + let secret_key = self.user_secret_key.chacha20_secret_key(); + let (cipher, cipher_nonce) = NKodeCipher::new(policy, &secret_key); + let passcode_idx: Vec = selected_icons + .iter() + .filter_map(|x1| self.icons.iter().position(|x| x.id() == x1)) + .collect(); + let ciphered_nkode = cipher.encipher(&passcode_idx).unwrap(); + let data = CodeLoginData{ + mask: ciphered_nkode.mask.clone(), + icons: self.icons.clone(), + cipher_nonce, + icon_nonce: self.icon_nonce.clone(), + keypad: keypad.clone(), + }; + self.api.register_code(&self.email, &ciphered_nkode.passcode, &self.key_login, &data).await?; + Ok(ClientAppCodeLogin{ + api: self.api, + mask: ciphered_nkode.mask, + email: self.email, + keypad, + cipher, + icons: self.icons + }) + } +} + +pub struct ClientAppCodeLogin { + api: S, + email: Email, + mask: Vec, + icons: Vec, + keypad: Keypad, + cipher: NKodeCipher +} + +impl ClientAppCodeLogin { + pub fn sort_icons(&self) -> Vec { + nkode_rs::tensor::reorder( + &self.icons, + self.keypad.indices() + ).unwrap().to_vec() + } + + pub async fn login(&self, selected_keys: &Vec) -> Result { + let passcode = self.cipher.decipher(selected_keys, self.keypad.indices(), &self.mask).map_err(|e| format!("invalid keys: {e}"))?; + self.api.login_code(&self.email, &passcode).await + } +} \ No newline at end of file diff --git a/src/models/app.rs b/src/models/app.rs new file mode 100644 index 0000000..ce2cc7d --- /dev/null +++ b/src/models/app.rs @@ -0,0 +1,55 @@ +use nkode_rs::nkode_core::chacha20prng::Nonce; use nkode_rs::nkode_core::keypad::Keypad; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use getset::Getters; +use nkode_rs::from_bytes::FromBytes; +use crate::models::email::Email; +use crate::models::opaque::OpaqueSessionKey; + +#[allow(dead_code)] +struct LoginSession { + email: Email, + session_id: Uuid, + session_key: OpaqueSessionKey, +} + +pub struct KeyLoginSession(LoginSession); +pub struct CodeLoginSession(LoginSession); + +pub const ICON_ID_SIZE: usize = 32; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IconID([u8; 32]); + +impl FromBytes for IconID { + fn from_array(arr: [u8; ICON_ID_SIZE]) -> Self { + Self(arr) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Getters)] +pub struct Icon { + #[get = "pub"] + id: IconID, + #[get = "pub"] + data: Vec, +} + +impl Icon { + pub fn update_id(&mut self, new_id: IconID) { + self.id = new_id + } +} + +#[derive(Debug, Clone, Getters, Serialize, Deserialize)] +pub struct CodeLoginData { + #[get = "pub"] + pub(crate) mask: Vec, + #[get = "pub"] + pub(crate) icons: Vec, + #[get = "pub"] + pub(crate) cipher_nonce: Nonce, + #[get = "pub"] + pub(crate) icon_nonce: Nonce, + #[get = "pub"] + pub(crate) keypad: Keypad, +} diff --git a/src/models/email.rs b/src/models/email.rs new file mode 100644 index 0000000..69a5bb4 --- /dev/null +++ b/src/models/email.rs @@ -0,0 +1,110 @@ +use std::{fmt, str::FromStr}; + +use email_address::{EmailAddress, Options}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Email(String); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmailError { + Empty, + InvalidUtf8, + InvalidFormat, +} + +impl Email { + pub fn new(s: impl AsRef) -> Result { + let s = s.as_ref().trim(); + if s.is_empty() { + return Err(EmailError::Empty); + } + + // Strict "address only" validation (no "Name ") + EmailAddress::parse_with_options(s, Options::default().without_display_text()) + .map_err(|_| EmailError::InvalidFormat)?; + + Ok(Self(s.to_owned())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn into_inner(self) -> String { + self.0 + } + + pub fn into_bytes(self) -> Vec { + self.0.into_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let s = std::str::from_utf8(bytes).map_err(|_| EmailError::InvalidUtf8)?; + Self::new(s) + } +} + +/* ---- std traits ---- */ + +impl fmt::Display for Email { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for Email { + type Err = EmailError; + fn from_str(s: &str) -> Result { + Email::new(s) + } +} + +impl TryFrom for Email { + type Error = EmailError; + fn try_from(value: String) -> Result { + Email::new(value) + } +} + +impl TryFrom<&str> for Email { + type Error = EmailError; + fn try_from(value: &str) -> Result { + Email::new(value) + } +} + +impl TryFrom<&[u8]> for Email { + type Error = EmailError; + fn try_from(value: &[u8]) -> Result { + Email::from_bytes(value) + } +} + +impl TryFrom> for Email { + type Error = EmailError; + fn try_from(value: Vec) -> Result { + Email::from_bytes(&value) + } +} + +impl AsRef for Email { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for Email { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From for Vec { + fn from(value: Email) -> Self { + value.into_bytes() + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 0551a7c..b3406f2 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1,3 @@ -pub mod opaque; \ No newline at end of file +pub mod opaque; +pub mod app; +pub mod email; \ No newline at end of file diff --git a/src/models/opaque.rs b/src/models/opaque.rs index 5c672cb..ca8fc95 100644 --- a/src/models/opaque.rs +++ b/src/models/opaque.rs @@ -5,10 +5,36 @@ use opaque_ke::CipherSuite; use opaque_ke::argon2::Argon2; use opaque_ke::generic_array::GenericArray; use uuid::Uuid; +use nkode_rs::nkode_core::chacha20prng::SecretKey; +use zeroize::Zeroizing; +use nkode_rs::from_bytes::FromBytes; -pub const NONCE_SIZE: usize = 12; -pub const SESSION_KEY_SIZE: usize = 32; -pub const SECRET_KEY_SIZE: usize = 16; +const USER_KEY_SIZE: usize = 16; + +pub struct UserSecretKey(Zeroizing<[u8; USER_KEY_SIZE]>); + +impl UserSecretKey { + pub fn chacha20_secret_key(&self) -> SecretKey { + let out = blake3::derive_key("your-app chacha20 secret key v1", &self.0.as_slice()); + SecretKey::from_bytes(&out).unwrap() + } +} + +impl FromBytes for UserSecretKey { + fn from_array(arr: [u8; USER_KEY_SIZE]) -> Self { + Self(Zeroizing::new(arr)) + } +} + +const OPAQUE_SESSION_KEY_SIZE: usize = 32; + +pub struct OpaqueSessionKey(Zeroizing<[u8; OPAQUE_SESSION_KEY_SIZE]>); + +impl FromBytes for OpaqueSessionKey { + fn from_array(arr: [u8; OPAQUE_SESSION_KEY_SIZE]) -> Self { + Self(Zeroizing::new(arr)) + } +} pub struct NKodeCipherSuite; diff --git a/src/opaque/client.rs b/src/opaque/client.rs index f56a346..e3f3da4 100644 --- a/src/opaque/client.rs +++ b/src/opaque/client.rs @@ -9,7 +9,7 @@ use opaque_ke::{ RegistrationRequest, }; -use crate::models::opaque::{RegisterSession, LoginSession, NKodeCipherSuite, PasswordFile}; +use crate::models::opaque::{RegisterSession, LoginSession, NKodeCipherSuite, PasswordFile, OpaqueSessionKey}; @@ -72,7 +72,7 @@ pub trait ServerConnectionLogin { &mut self, session_id: &Uuid, message: &CredentialFinalization, - ) -> Result<(), ClientAuthError>; + ) -> Result; } // --- OPAQUE client driver --- diff --git a/src/opaque/server.rs b/src/opaque/server.rs index 9b82ff8..8014a62 100644 --- a/src/opaque/server.rs +++ b/src/opaque/server.rs @@ -1,10 +1,11 @@ +use nkode_rs::from_bytes::FromBytes; use opaque_ke::{ rand::rngs::OsRng, CredentialFinalization, CredentialRequest, RegistrationRequest, ServerLogin, ServerLoginParameters, ServerRegistration, }; use uuid::Uuid; -use crate::models::opaque::{LoginSession, NKodeCipherSuite, NKodeServerSetup, PasswordFile, RegisterSession}; +use crate::models::opaque::{LoginSession, NKodeCipherSuite, NKodeServerSetup, OpaqueSessionKey, PasswordFile, RegisterSession}; use crate::repository::opaque::repos::{OpaqueDatabaseRepo, AuthRepoError, OpaqueSessionRepo}; pub struct RegCache { @@ -141,7 +142,7 @@ impl OpaqueAuth { &mut self, session_id: &Uuid, finalize: CredentialFinalization, - ) -> Result, String> { + ) -> Result { let cache = self.session .get_login_session(session_id) .map_err(|e| format!("get login session: {e}"))?; @@ -154,6 +155,6 @@ impl OpaqueAuth { .clear_login_session(session_id) .map_err(|e| format!("clear login session: {e}"))?; - Ok(finish.session_key.to_vec()) + Ok(OpaqueSessionKey::from_bytes(&finish.session_key.to_vec()).unwrap()) } } diff --git a/src/repository/opaque/in_memory/in_memory_transport.rs b/src/repository/opaque/in_memory/in_memory_transport.rs index f89b7c0..4c6ce4f 100644 --- a/src/repository/opaque/in_memory/in_memory_transport.rs +++ b/src/repository/opaque/in_memory/in_memory_transport.rs @@ -4,7 +4,7 @@ use tokio::sync::Mutex; use std::sync::Arc; use uuid::Uuid; use opaque_ke::{CredentialFinalization, CredentialRequest, RegistrationRequest}; -use crate::models::opaque::{LoginSession, NKodeCipherSuite, NKodeServerSetup, PasswordFile, RegisterSession}; +use crate::models::opaque::{LoginSession, NKodeCipherSuite, NKodeServerSetup, OpaqueSessionKey, PasswordFile, RegisterSession}; use crate::opaque::client::{ClientAuthError, ServerConnectionLogin, ServerConnectionRegister}; use crate::opaque::server::{OpaqueAuth, CredKind, Key, Code}; use crate::repository::opaque::in_memory::in_memory_auth_repo::InMemoryAuthRepo; @@ -77,15 +77,15 @@ where &mut self, session_id: &Uuid, message: &CredentialFinalization, - ) -> Result<(), ClientAuthError> { + ) -> Result { // Server computes its own session key too; we just need it to validate and complete. - let _server_session_key = self + let key = self .auth .login_finish::(session_id, message.clone()) .await .map_err(|e| ClientAuthError::Transport(e))?; - Ok(()) + Ok(key) } } @@ -151,12 +151,12 @@ where &mut self, session_id: &Uuid, message: &CredentialFinalization, - ) -> Result<(), ClientAuthError> { + ) -> Result { let mut guard = self.inner.lock().await; - let _ = guard + let key = guard .login_finish::(session_id, message.clone()) .await .map_err(ClientAuthError::Transport)?; - Ok(()) + Ok(key) } }