sofie_identity/two_factor/
mod.rs1mod challenge;
2mod code;
3mod qr;
4mod screen;
5
6use std::time::Duration;
7
8use sofie::core::Instant;
9use sofie::db::{Outcome, Plan, Reply, Stamp};
10use sofie::http::Sealing;
11use sofie::prelude::*;
12
13pub(crate) use challenge::{FactorCode, asking, routes as challenge_routes};
14pub use screen::routes;
15
16use crate::AccountId;
17
18const SECRET: &str = "sofie_identity.totp";
19const RECOVERY: &str = "sofie_identity.recovery";
20const MAXIMUM_FAILURES: i32 = 5;
22const FAILURE_WINDOW: Duration = Duration::from_mins(15);
23const NOT_RIGHT: &str =
24 "is not the current code of your authenticator app or an unused recovery code";
25const TOO_MANY: &str = "Too many wrong codes; try again in fifteen minutes.";
26
27#[derive(Aggregate)]
30pub struct SecondFactor {
31 meta: Meta,
32 #[owner]
33 account: AccountId,
34 secret: String,
35 confirmed_at: Option<Instant>,
37 last_step: i64,
39 recovery: String,
41 pending_recovery: Option<String>,
43 failures: i32,
44 failing_since: Option<Instant>,
45}
46
47impl SecondFactor {
48 pub(crate) fn is_confirmed(&self) -> bool {
49 self.confirmed_at.is_some()
50 }
51
52 fn key(&self, sealing: &Sealing) -> Result<Vec<u8>> {
53 sealing.open(SECRET, &self.secret).ok_or_else(|| {
54 Error::internal_message("a second factor's key does not open with the app's keys")
55 })
56 }
57
58 pub(crate) fn prove(
60 &mut self,
61 code: &str,
62 sealing: &Sealing,
63 now: Instant,
64 ) -> Result<Result<()>> {
65 if self.failing(now)? && self.failures >= MAXIMUM_FAILURES {
66 return Ok(Err(Error::Rule(TOO_MANY.to_owned())));
67 }
68 if self.used_recovery(code) {
69 return Ok(Ok(()));
70 }
71 let key = self.key(sealing)?;
72 self.secret = sealing.seal(SECRET, &key)?;
74 let now_seconds = now.timestamp().as_second();
75 if let Some(step) = code::matching_step(&key, code, now_seconds, self.last_step) {
76 self.last_step = step;
77 self.failures = 0;
78 self.failing_since = None;
79 return Ok(Ok(()));
80 }
81 self.failed(now)?;
82 Ok(Err(Error::invalid("code", NOT_RIGHT)))
83 }
84
85 fn used_recovery(&mut self, code: &str) -> bool {
86 let digest = code::recovery_digest(code);
87 let unused: Vec<&str> = self.recovery.split_whitespace().collect();
88 if !self.is_confirmed() || !unused.contains(&digest.as_str()) {
89 return false;
90 }
91 self.recovery = unused
92 .into_iter()
93 .filter(|kept| *kept != digest)
94 .collect::<Vec<_>>()
95 .join(" ");
96 true
97 }
98
99 fn failing(&self, now: Instant) -> Result<bool> {
100 match self.failing_since {
101 Some(since) => Ok(since.checked_add(FAILURE_WINDOW)? > now),
102 None => Ok(false),
103 }
104 }
105
106 fn failed(&mut self, now: Instant) -> Result<()> {
107 if !self.failing(now)? {
108 self.failing_since = Some(now);
109 self.failures = 0;
110 }
111 self.failures = self.failures.saturating_add(1);
112 Ok(())
113 }
114}
115
116pub(crate) struct Proof<T> {
119 factor: SecondFactor,
120 passed: Result<T>,
121}
122
123impl<T: Outcome> Outcome for Proof<T> {
124 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
125 self.factor.plan(plan)?;
126 match &self.passed {
127 Ok(passed) => passed.plan(plan),
128 Err(_) => Ok(()),
129 }
130 }
131
132 fn committed(&mut self, stamp: Stamp) {
133 self.factor.committed(stamp);
134 if let Ok(passed) = &mut self.passed {
135 passed.committed(stamp);
136 }
137 }
138
139 fn reply(self) -> Result<Reply> {
140 self.passed.map(|_| Reply::Empty)
141 }
142}
143
144#[cfg(feature = "account-data")]
146pub(crate) async fn exported(read: &Read, account: AccountId) -> Result<sofie::serde_json::Value> {
147 Ok(of(read, account)
148 .await?
149 .map_or(sofie::serde_json::Value::Null, |factor| {
150 sofie::serde_json::json!({
151 "on": factor.is_confirmed(),
152 "confirmed_at": factor.confirmed_at,
153 })
154 }))
155}
156
157pub(crate) async fn of(read: &Read, account: AccountId) -> Result<Option<SecondFactor>> {
158 SecondFactor::query()
159 .where_(SecondFactor::ACCOUNT.eq(account))
160 .first(read)
161 .await
162}
163
164pub(crate) async fn required(read: &Read, account: AccountId) -> Result<Option<SecondFactor>> {
165 Ok(of(read, account).await?.filter(SecondFactor::is_confirmed))
166}
167
168pub(crate) async fn removing(read: &Read, account: AccountId) -> Result<Vec<SecondFactor>> {
169 let mut factors: Vec<_> = of(read, account).await?.into_iter().collect();
170 for factor in &mut factors {
171 factor.secret = String::new();
172 factor.confirmed_at = None;
173 factor.recovery = String::new();
174 factor.pending_recovery = None;
175 }
176 Ok(factors)
177}