From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id BAB8F694A3 for ; Mon, 18 Jan 2021 12:46:49 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B8B41125BE for ; Mon, 18 Jan 2021 12:46:49 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 04977125B5 for ; Mon, 18 Jan 2021 12:46:49 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id BDBB346031 for ; Mon, 18 Jan 2021 12:46:48 +0100 (CET) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Mon, 18 Jan 2021 12:46:46 +0100 Message-Id: <20210118114647.632-1-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.050 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [PATCH backup 1/2] tfa: remember recovery indices X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 18 Jan 2021 11:46:49 -0000 and tell the client which keys are still available rather than just yes/no/low Signed-off-by: Wolfgang Bumiller --- src/config/tfa.rs | 76 ++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/config/tfa.rs b/src/config/tfa.rs index e0f2fcfe..5d01ea82 100644 --- a/src/config/tfa.rs +++ b/src/config/tfa.rs @@ -1088,7 +1088,7 @@ impl TfaUserData { #[derive(Deserialize, Serialize)] pub struct Recovery { secret: String, - entries: Vec, + entries: Vec>, } impl Recovery { @@ -1116,7 +1116,7 @@ impl Recovery { AsHex(&b[6..8]), ); - this.entries.push(this.hash(entry.as_bytes())?); + this.entries.push(Some(this.hash(entry.as_bytes())?)); original.push(entry); } @@ -1138,31 +1138,32 @@ impl Recovery { Ok(AsHex(&hmac).to_string()) } - /// Shortcut to get the count. - fn len(&self) -> usize { - self.entries.len() + /// Iterator over available keys. + fn available(&self) -> impl Iterator { + self.entries.iter().filter_map(Option::as_deref) } - /// Check if this entry is empty. - fn is_empty(&self) -> bool { - self.entries.is_empty() + /// Count the available keys. + fn count_available(&self) -> usize { + self.available().count() } /// Convenience serde method to check if either the option is `None` or the content `is_empty`. fn option_is_empty(this: &Option) -> bool { - this.as_ref().map_or(true, Self::is_empty) + this.as_ref() + .map_or(true, |this| this.count_available() == 0) } /// Verify a key and remove it. Returns whether the key was valid. Errors on openssl errors. fn verify(&mut self, key: &str) -> Result { let hash = self.hash(key.as_bytes())?; - Ok(match self.entries.iter().position(|entry| *entry == hash) { - Some(index) => { - self.entries.remove(index); - true + for entry in &mut self.entries { + if entry.as_ref() == Some(&hash) { + *entry = None; + return Ok(true); } - None => false, - }) + } + Ok(false) } } @@ -1283,45 +1284,38 @@ pub fn verify_challenge( } /// Used to inform the user about the recovery code status. -#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum RecoveryState { - Unavailable, - Low, - Available, -} +/// +/// This contains the available key indices. +#[derive(Clone, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct RecoveryState(Vec); impl RecoveryState { - fn from_count(count: usize) -> Self { - match count { - 0 => RecoveryState::Unavailable, - 1..=3 => RecoveryState::Low, - _ => RecoveryState::Available, - } - } - - // serde needs `&self` but this is a tiny Copy type, so we mark this as inline - #[inline] fn is_unavailable(&self) -> bool { - *self == RecoveryState::Unavailable - } -} - -impl Default for RecoveryState { - fn default() -> Self { - RecoveryState::Unavailable + self.0.is_empty() } } impl From<&Option> for RecoveryState { fn from(r: &Option) -> Self { match r { - Some(r) => Self::from_count(r.len()), - None => RecoveryState::Unavailable, + Some(r) => Self::from(r), + None => Self::default(), } } } +impl From<&Recovery> for RecoveryState { + fn from(r: &Recovery) -> Self { + Self( + r.entries + .iter() + .enumerate() + .filter_map(|(idx, key)| if key.is_some() { Some(idx) } else { None }) + .collect(), + ) + } +} + /// When sending a TFA challenge to the user, we include information about what kind of challenge /// the user may perform. If webauthn credentials are available, a webauthn challenge will be /// included. -- 2.20.1