tfa: add 'created' timestamp to entries

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2021-01-18 13:50:00 +01:00 committed by Thomas Lamprecht
parent bad6e32075
commit ad5cee1d22
2 changed files with 32 additions and 12 deletions

View File

@ -82,12 +82,12 @@ fn to_data(data: TfaUserData) -> Vec<TypedTfaInfo> {
data.totp.len() data.totp.len()
+ data.u2f.len() + data.u2f.len()
+ data.webauthn.len() + data.webauthn.len()
+ if data.has_recovery() { 1 } else { 0 }, + if data.recovery().is_some() { 1 } else { 0 },
); );
if data.has_recovery() { if let Some(recovery) = data.recovery() {
out.push(TypedTfaInfo { out.push(TypedTfaInfo {
ty: TfaType::Recovery, ty: TfaType::Recovery,
info: TfaInfo::recovery(), info: TfaInfo::recovery(recovery.created),
}) })
} }
for entry in data.totp { for entry in data.totp {
@ -184,10 +184,12 @@ fn get_tfa_entry(userid: Userid, id: String) -> Result<TypedTfaInfo, Error> {
entry.map(|(ty, index, _)| (ty, index)) entry.map(|(ty, index, _)| (ty, index))
} { } {
Some((TfaType::Recovery, _)) => { Some((TfaType::Recovery, _)) => {
return Ok(TypedTfaInfo { if let Some(recovery) = user_data.recovery() {
ty: TfaType::Recovery, return Ok(TypedTfaInfo {
info: TfaInfo::recovery(), ty: TfaType::Recovery,
}) info: TfaInfo::recovery(recovery.created),
});
}
} }
Some((TfaType::Totp, index)) => { Some((TfaType::Totp, index)) => {
return Ok(TypedTfaInfo { return Ok(TypedTfaInfo {

View File

@ -345,6 +345,9 @@ pub struct TfaInfo {
/// User chosen description for this entry. /// User chosen description for this entry.
pub description: String, pub description: String,
/// Creation time of this entry as unix epoch.
pub created: i64,
/// Whether this TFA entry is currently enabled. /// Whether this TFA entry is currently enabled.
#[serde(skip_serializing_if = "is_default_tfa_enable")] #[serde(skip_serializing_if = "is_default_tfa_enable")]
#[serde(default = "default_tfa_enable")] #[serde(default = "default_tfa_enable")]
@ -353,11 +356,12 @@ pub struct TfaInfo {
impl TfaInfo { impl TfaInfo {
/// For recovery keys we have a fixed entry. /// For recovery keys we have a fixed entry.
pub(crate) fn recovery() -> Self { pub(crate) fn recovery(created: i64) -> Self {
Self { Self {
id: "recovery".to_string(), id: "recovery".to_string(),
description: "recovery keys".to_string(), description: "recovery keys".to_string(),
enable: true, enable: true,
created,
} }
} }
} }
@ -383,6 +387,7 @@ impl<T> TfaEntry<T> {
id: Uuid::generate().to_string(), id: Uuid::generate().to_string(),
enable: true, enable: true,
description, description,
created: proxmox::tools::time::epoch_i64(),
}, },
entry, entry,
} }
@ -748,9 +753,13 @@ pub struct TfaUserData {
} }
impl TfaUserData { impl TfaUserData {
/// Shortcut for the option type. /// Shortcut to get the recovery entry only if it is not empty!
pub fn has_recovery(&self) -> bool { pub fn recovery(&self) -> Option<&Recovery> {
!Recovery::option_is_empty(&self.recovery) if Recovery::option_is_empty(&self.recovery) {
None
} else {
self.recovery.as_ref()
}
} }
/// `true` if no second factors exist /// `true` if no second factors exist
@ -758,7 +767,7 @@ impl TfaUserData {
self.totp.is_empty() self.totp.is_empty()
&& self.u2f.is_empty() && self.u2f.is_empty()
&& self.webauthn.is_empty() && self.webauthn.is_empty()
&& !self.has_recovery() && self.recovery().is_none()
} }
/// Find an entry by id, except for the "recovery" entry which we're currently treating /// Find an entry by id, except for the "recovery" entry which we're currently treating
@ -1087,8 +1096,16 @@ impl TfaUserData {
/// Recovery entries. We use HMAC-SHA256 with a random secret as a salted hash replacement. /// Recovery entries. We use HMAC-SHA256 with a random secret as a salted hash replacement.
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct Recovery { pub struct Recovery {
/// "Salt" used for the key HMAC.
secret: String, secret: String,
/// Recovery key entries are HMACs of the original data. When used up they will become `None`
/// since the user is presented an enumerated list of codes, so we know the indices of used and
/// unused codes.
entries: Vec<Option<String>>, entries: Vec<Option<String>>,
/// Creation timestamp as a unix epoch.
pub created: i64,
} }
impl Recovery { impl Recovery {
@ -1101,6 +1118,7 @@ impl Recovery {
let mut this = Self { let mut this = Self {
secret: AsHex(&secret).to_string(), secret: AsHex(&secret).to_string(),
entries: Vec::with_capacity(10), entries: Vec::with_capacity(10),
created: proxmox::tools::time::epoch_i64(),
}; };
let mut original = Vec::new(); let mut original = Vec::new();