feat(core): flesh out TotpCore + TotpConfig + TotpAlgorithm + TotpKind
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,38 @@
|
|||||||
|
//! TOTP: standalone 2FA item type. Also reused as TotpConfig field on Login.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct TotpCore {}
|
pub struct TotpCore {
|
||||||
|
pub config: TotpConfig,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub issuer: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TotpConfig {}
|
pub struct TotpConfig {
|
||||||
|
/// Raw bytes of the TOTP secret (decoded from base32 when imported).
|
||||||
|
pub secret: Zeroizing<Vec<u8>>,
|
||||||
|
pub algorithm: TotpAlgorithm,
|
||||||
|
pub digits: u8,
|
||||||
|
pub period_seconds: u32,
|
||||||
|
pub kind: TotpKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TotpConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
secret: Zeroizing::new(Vec::new()),
|
||||||
|
algorithm: TotpAlgorithm::Sha1,
|
||||||
|
digits: 6,
|
||||||
|
period_seconds: 30,
|
||||||
|
kind: TotpKind::Totp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -15,7 +43,7 @@ pub enum TotpAlgorithm {
|
|||||||
Sha512,
|
Sha512,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum TotpKind {
|
pub enum TotpKind {
|
||||||
Totp,
|
Totp,
|
||||||
@@ -26,3 +54,55 @@ pub enum TotpKind {
|
|||||||
impl Default for TotpKind {
|
impl Default for TotpKind {
|
||||||
fn default() -> Self { TotpKind::Totp }
|
fn default() -> Self { TotpKind::Totp }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totp_default_is_sha1_6_30_totp() {
|
||||||
|
let cfg = TotpConfig::default();
|
||||||
|
assert_eq!(cfg.algorithm, TotpAlgorithm::Sha1);
|
||||||
|
assert_eq!(cfg.digits, 6);
|
||||||
|
assert_eq!(cfg.period_seconds, 30);
|
||||||
|
assert_eq!(cfg.kind, TotpKind::Totp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totp_round_trip() {
|
||||||
|
let core = TotpCore {
|
||||||
|
config: TotpConfig {
|
||||||
|
secret: Zeroizing::new(vec![0x12, 0x34, 0x56]),
|
||||||
|
algorithm: TotpAlgorithm::Sha256,
|
||||||
|
digits: 8,
|
||||||
|
period_seconds: 60,
|
||||||
|
kind: TotpKind::Totp,
|
||||||
|
},
|
||||||
|
issuer: Some("github".into()),
|
||||||
|
label: Some("alice@github".into()),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&core).unwrap();
|
||||||
|
let parsed: TotpCore = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(parsed.config.digits, 8);
|
||||||
|
assert_eq!(parsed.config.algorithm, TotpAlgorithm::Sha256);
|
||||||
|
assert_eq!(parsed.issuer.as_deref(), Some("github"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hotp_carries_counter() {
|
||||||
|
let cfg = TotpConfig { kind: TotpKind::Hotp { counter: 42 }, ..TotpConfig::default() };
|
||||||
|
let json = serde_json::to_string(&cfg).unwrap();
|
||||||
|
let parsed: TotpConfig = serde_json::from_str(&json).unwrap();
|
||||||
|
match parsed.kind {
|
||||||
|
TotpKind::Hotp { counter } => assert_eq!(counter, 42),
|
||||||
|
other => panic!("expected Hotp, got {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn steam_kind_serializes() {
|
||||||
|
let cfg = TotpConfig { kind: TotpKind::Steam, ..TotpConfig::default() };
|
||||||
|
let json = serde_json::to_string(&cfg).unwrap();
|
||||||
|
assert!(json.contains("steam"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user