Show OTP code validity in frontends
This commit is contained in:
@@ -426,8 +426,39 @@ impl OtpWriteOutcome {
|
||||
|
||||
pub struct OtpCodeOutcome {
|
||||
code: SecretBytes,
|
||||
counter: Option<u64>,
|
||||
valid_until: Option<u64>,
|
||||
validity: OtpCodeValidity,
|
||||
}
|
||||
|
||||
/// Storage-owned validity information for presenting an OTP code.
|
||||
///
|
||||
/// Frontends use this value instead of deriving TOTP periods or inferring HOTP
|
||||
/// behavior from display strings. A timed code carries its exclusive Unix-time
|
||||
/// boundary, while a counter-based code identifies the committed HOTP counter.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum OtpCodeValidity {
|
||||
Timed { valid_until: u64 },
|
||||
CounterBased { counter: u64 },
|
||||
}
|
||||
|
||||
impl OtpCodeValidity {
|
||||
pub fn valid_until(self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Timed { valid_until } => Some(valid_until),
|
||||
Self::CounterBased { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn counter(self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Timed { .. } => None,
|
||||
Self::CounterBased { counter } => Some(counter),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remaining_at(self, unix_seconds: u64) -> Option<u64> {
|
||||
self.valid_until()
|
||||
.map(|valid_until| valid_until.saturating_sub(unix_seconds))
|
||||
}
|
||||
}
|
||||
|
||||
impl OtpCodeOutcome {
|
||||
@@ -436,16 +467,19 @@ impl OtpCodeOutcome {
|
||||
}
|
||||
|
||||
pub fn counter(&self) -> Option<u64> {
|
||||
self.counter
|
||||
self.validity.counter()
|
||||
}
|
||||
|
||||
pub fn valid_until(&self) -> Option<u64> {
|
||||
self.valid_until
|
||||
self.validity.valid_until()
|
||||
}
|
||||
|
||||
pub fn validity(&self) -> OtpCodeValidity {
|
||||
self.validity
|
||||
}
|
||||
|
||||
pub fn remaining_at(&self, unix_seconds: u64) -> Option<u64> {
|
||||
self.valid_until
|
||||
.map(|valid_until| valid_until.saturating_sub(unix_seconds))
|
||||
self.validity.remaining_at(unix_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,8 +488,7 @@ impl fmt::Debug for OtpCodeOutcome {
|
||||
formatter
|
||||
.debug_struct("OtpCodeOutcome")
|
||||
.field("code", &"[REDACTED]")
|
||||
.field("counter", &self.counter)
|
||||
.field("valid_until", &self.valid_until)
|
||||
.field("validity", &self.validity)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -657,13 +690,12 @@ impl<'a> OtpService<'a> {
|
||||
let period = uri.period().ok_or(OtpError::NotTotp)?;
|
||||
return Ok(OtpCodeOutcome {
|
||||
code: uri.code_at(unix_seconds)?,
|
||||
counter: None,
|
||||
valid_until: Some(
|
||||
(unix_seconds / period)
|
||||
validity: OtpCodeValidity::Timed {
|
||||
valid_until: (unix_seconds / period)
|
||||
.checked_add(1)
|
||||
.and_then(|counter| counter.checked_mul(period))
|
||||
.ok_or(OtpError::CounterOverflow)?,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
let entry = path.to_string();
|
||||
@@ -713,14 +745,15 @@ impl<'a> OtpService<'a> {
|
||||
match uri.kind() {
|
||||
OtpKind::Totp => Ok(OtpCodeOutcome {
|
||||
code: uri.code_at(unix_seconds)?,
|
||||
counter: None,
|
||||
valid_until: Some({
|
||||
let period = uri.period().ok_or(OtpError::NotTotp)?;
|
||||
(unix_seconds / period)
|
||||
.checked_add(1)
|
||||
.and_then(|counter| counter.checked_mul(period))
|
||||
.ok_or(OtpError::CounterOverflow)?
|
||||
}),
|
||||
validity: OtpCodeValidity::Timed {
|
||||
valid_until: {
|
||||
let period = uri.period().ok_or(OtpError::NotTotp)?;
|
||||
(unix_seconds / period)
|
||||
.checked_add(1)
|
||||
.and_then(|counter| counter.checked_mul(period))
|
||||
.ok_or(OtpError::CounterOverflow)?
|
||||
},
|
||||
},
|
||||
}),
|
||||
OtpKind::Hotp => {
|
||||
let (counter, incremented) = uri.incremented_hotp()?;
|
||||
@@ -742,8 +775,7 @@ impl<'a> OtpService<'a> {
|
||||
)?;
|
||||
Ok(OtpCodeOutcome {
|
||||
code,
|
||||
counter: Some(counter),
|
||||
valid_until: None,
|
||||
validity: OtpCodeValidity::CounterBased { counter },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ironstorage::{
|
||||
command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest},
|
||||
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||
git::{GitIdentity, GitRepository},
|
||||
otp::{OtpAlgorithm, OtpError, OtpInput, OtpKind, OtpService, OtpUri},
|
||||
otp::{OtpAlgorithm, OtpCodeValidity, OtpError, OtpInput, OtpKind, OtpService, OtpUri},
|
||||
recipient::RecipientPolicyManager,
|
||||
repository::{EntryPath, Repository, SecretBytes},
|
||||
write::{EntryCommit, EntryCommitError, EntryCommitter, OverwriteDecision},
|
||||
@@ -478,7 +478,9 @@ fn automatic_code_supports_pass_diff_config_and_opens_git_only_for_hotp() -> Tes
|
||||
let totp = service.code_automatic("otp/totp", 59, None, &mut provider)?;
|
||||
assert_eq!(totp.counter(), None);
|
||||
assert_eq!(totp.valid_until(), Some(60));
|
||||
assert_eq!(totp.validity(), OtpCodeValidity::Timed { valid_until: 60 });
|
||||
assert_eq!(totp.remaining_at(59), Some(1));
|
||||
assert_eq!(totp.remaining_at(60), Some(0));
|
||||
assert_eq!(repository.read_entry(&totp_path)?, totp_before);
|
||||
let git = GitRepository::open(&repository, identity.clone())?;
|
||||
assert_eq!(git.log(None)?.len(), initial_commits);
|
||||
@@ -487,6 +489,11 @@ fn automatic_code_supports_pass_diff_config_and_opens_git_only_for_hotp() -> Tes
|
||||
let hotp = service.code_automatic("otp/hotp", 0, None, &mut provider)?;
|
||||
assert_eq!(hotp.counter(), Some(1));
|
||||
assert_eq!(hotp.valid_until(), None);
|
||||
assert_eq!(
|
||||
hotp.validity(),
|
||||
OtpCodeValidity::CounterBased { counter: 1 }
|
||||
);
|
||||
assert_eq!(hotp.remaining_at(0), None);
|
||||
assert_eq!(service.uri("otp/hotp", &mut provider)?.counter(), Some(1));
|
||||
let git = GitRepository::open(&repository, identity)?;
|
||||
assert_eq!(git.log(None)?.len(), initial_commits + 1);
|
||||
|
||||
Reference in New Issue
Block a user